Popular Searches
Popular Course Categories
Popular Courses

Displaying Database and API Data in Dynamic Lists

Displaying Database and API Data in Dynamic Lists

Lists, RecyclerView, and Data Presentation

Displaying Database and API Data in Dynamic Lists

Course: Android App Development

Topic: Displaying Database and API Data in Dynamic Lists

Course Link: Android App Development Training

Demo Link: Register for a Course Demo

Website: JustAcademy


1. Introduction to Dynamic Lists

Dynamic lists are user interface components that display collections of data that can change while an application is running. Unlike static lists, which contain predefined items, dynamic lists retrieve information from sources such as local databases, remote APIs, or application memory.

Android applications commonly use RecyclerView to display dynamic lists because it efficiently reuses item views and supports large collections of data.

For example, a shopping application may retrieve product details from a REST API, save the results in a local database, and display the products in a RecyclerView. When new products arrive or existing products change, the displayed list can be updated accordingly.

Real-World Applications

  • E-commerce: Display products, prices, and availability from an API.
  • Messaging applications: Show conversations stored in a local database.
  • News applications: Retrieve articles from a server and display them dynamically.
  • Education applications: Display courses, lessons, and student progress.
  • Food delivery applications: Show restaurants and menu items retrieved from remote services.
  • Task management applications: Display tasks saved locally and update the list when tasks are completed.

2. Learning Objectives

After studying this topic, learners should be able to:

  • Understand dynamic lists and their role in Android applications.
  • Display database records using RecyclerView.
  • Retrieve JSON data from a REST API.
  • Convert API responses into Kotlin data objects.
  • Update RecyclerView when the underlying data changes.
  • Use Kotlin coroutines for asynchronous operations.
  • Implement loading, empty, success, and error states.
  • Combine remote API data with local database caching.
  • Understand pagination, refresh operations, and basic data synchronization.

3. Understanding Data Sources

Before implementing a dynamic list, it is important to understand where its data originates.

3.1 Local Database

A local database stores structured information on the user's device. Android applications can use Room, an abstraction layer over SQLite, to store and retrieve structured data with entities, data access objects (DAOs), and database classes.

Local database data is useful when an application needs persistent storage, fast access, or offline functionality.

3.2 Remote API

An API allows an application to communicate with a remote server. A REST API commonly returns data in JSON format over HTTP or HTTPS.

For example, a product API might return the following JSON response:

{
  "products": [
    {
      "id": 1,
      "title": "Android Development Guide",
      "price": 499
    },
    {
      "id": 2,
      "title": "Kotlin Programming",
      "price": 599
    }
  ]
}

3.3 In-Memory Data

In-memory data exists only while the application retains it in memory. It is useful for temporary lists, intermediate calculations, and UI state, but it should not be treated as persistent storage.

Comparison of Data Sources

Feature Local Database Remote API In-Memory List
Storage location Device Remote server Application memory
Internet required No Usually yes No
Persistent across restarts Yes Data depends on server availability Not guaranteed
Typical use Cached records and offline data Latest server data Temporary UI data

4. Understanding RecyclerView

RecyclerView is an Android UI component for displaying scrollable collections. It reuses item views as they move off-screen, reducing the cost of creating new views repeatedly.

Its main components include:

  • RecyclerView: The container that displays the list.
  • Adapter: Connects the dataset to the displayed item views.
  • ViewHolder: Holds references to the views inside an item layout.
  • LayoutManager: Determines how items are arranged.
  • Dataset: The collection of records displayed by the list.

A dynamic list usually follows this sequence:

  1. Retrieve records from a database or API.
  2. Convert the records into Kotlin objects.
  3. Store the current result in application state.
  4. Submit the new dataset to the RecyclerView adapter.
  5. Allow the adapter to update the visible items.

5. Creating a Kotlin Data Model

A data model represents one record in the list. Suppose an application displays products with an ID, name, and price.

data class Product(
    val id: Int,
    val title: String,
    val price: Double
)

This model can be used for data received from an API, although a real API response must be parsed or mapped into the model. When using Room, database entities are defined with the appropriate Room annotations.

6. Displaying Database Data with Room

Room provides a structured way to work with SQLite databases. Its main components are:

  • Entity: Defines a database table.
  • DAO: Defines methods for querying and modifying records.
  • RoomDatabase: Provides access to the database and its DAOs.

6.1 Creating a Room Entity

@Entity(tableName = "products")
data class ProductEntity(
    @PrimaryKey
    val id: Int,
    val title: String,
    val price: Double
)

The @Entity annotation marks the class as a Room table. The primary key uniquely identifies each product.

6.2 Creating a DAO

@Dao
interface ProductDao {

    @Query("SELECT * FROM products ORDER BY title ASC")
    fun observeProducts(): Flow<List<ProductEntity>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertProducts(
        products: List<ProductEntity>
    )

    @Query("DELETE FROM products")
    suspend fun clearProducts()
}

The observable query returns a Flow. When Room detects relevant changes to the products table, the query can emit an updated list. The insert method saves records, while the delete method removes all products.

Room database operations should not block the main thread. Kotlin coroutines and Flow provide asynchronous access patterns for database operations.

6.3 Creating the Database

@Database(
    entities = [ProductEntity::class],
    version = 1,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun productDao(): ProductDao
}

Build a database instance using the Room version and dependencies configured in your Android project.

val database = Room.databaseBuilder(
    applicationContext,
    AppDatabase::class.java,
    "shop_database"
).build()

val productDao = database.productDao()

In a real application, create the database through a dependency injection framework or a singleton provider instead of repeatedly constructing new instances.

7. Displaying Database Records in RecyclerView

The adapter displays each product in a row. For production applications, ListAdapter and DiffUtil are useful because they calculate differences between old and new datasets.

Assume an XML row layout named item_product.xml contains a TextView with the ID productTitle.

class ProductAdapter :
    ListAdapter<ProductEntity, ProductAdapter.ProductViewHolder>(
        object : DiffUtil.ItemCallback<ProductEntity>() {

            override fun areItemsTheSame(
                oldItem: ProductEntity,
                newItem: ProductEntity
            ): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(
                oldItem: ProductEntity,
                newItem: ProductEntity
            ): Boolean {
                return oldItem == newItem
            }
        }
    ) {

    class ProductViewHolder(
        itemView: View
    ) : RecyclerView.ViewHolder(itemView) {
        val title: TextView =
            itemView.findViewById(R.id.productTitle)
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): ProductViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_product, parent, false)

        return ProductViewHolder(view)
    }

    override fun onBindViewHolder(
        holder: ProductViewHolder,
        position: Int
    ) {
        holder.title.text = getItem(position).title
    }
}

When the adapter receives a new list through submitList(), DiffUtil compares the items and identifies the changes that need to be reflected in RecyclerView.

8. Observing Database Changes

When a database query returns Flow, the application can collect it from a lifecycle-aware coroutine scope and submit each emitted list to the adapter.

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        productDao.observeProducts().collect { products ->
            productAdapter.submitList(products)
        }
    }
}

This example assumes that the database and adapter have already been initialized. It also assumes that the necessary lifecycle and coroutine imports are included.

The benefit is that the list can update automatically when the database query emits new results, such as after a product is inserted or deleted.

9. Retrieving Data from a REST API

Remote APIs allow Android applications to display information managed by a server. A typical API workflow includes sending an HTTP request, receiving a response, parsing the response body, and displaying the resulting objects.

Common HTTP methods include:

Method Purpose Example
GET Retrieve records Get the product list
POST Create a record Add a new product
PUT/PATCH Update a record Update product details
DELETE Remove a record Delete a product

For a dynamic list, GET is commonly used to retrieve records from the server.

10. Defining API Response Models

Suppose the API returns a JSON object containing a list of products. Define Kotlin classes that match the JSON structure.

data class ProductResponse(
    val products: List<ApiProduct>
)

data class ApiProduct(
    val id: Int,
    val title: String,
    val price: Double
)

The model property names and data types must match the API response or be mapped using the JSON library's serialization annotations. If the API returns nullable or optional values, model them appropriately.

11. Creating a Retrofit API Service

Retrofit is a popular HTTP client library for Android applications. It allows developers to define API endpoints as Kotlin interfaces.

Add compatible Retrofit and JSON converter dependencies to the module-level Gradle file. The following example uses a Retrofit service with a JSON converter configured by the application.

interface ProductApiService {

    @GET("products")
    suspend fun getProducts(): ProductResponse
}

Configure Retrofit with the correct API base URL and JSON converter.

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(
        GsonConverterFactory.create()
    )
    .build()

val apiService =
    retrofit.create(ProductApiService::class.java)

Important: Replace the example base URL with the real HTTPS API endpoint. The API response must match the expected response model. Also add the Internet permission to the Android manifest when required:

<uses-permission
    android:name="android.permission.INTERNET" />

12. Fetching API Data Asynchronously

Network requests may take time to complete. They should not run synchronously on the main thread because doing so can make the application unresponsive.

Kotlin coroutines allow suspend functions to execute asynchronously while keeping the code readable.

lifecycleScope.launch {
    try {
        val response = apiService.getProducts()
        productAdapter.submitList(response.products.map {
            ProductEntity(
                id = it.id,
                title = it.title,
                price = it.price
            )
        })
    } catch (exception: IOException) {
        // Handle connectivity problems.
    } catch (exception: HttpException) {
        // Handle HTTP errors.
    }
}

This simplified example directly displays the API results. A complete screen should also manage loading indicators, empty states, HTTP errors, JSON parsing failures, and lifecycle-related UI state.

13. Updating the List with API Data

When a network request succeeds, the application converts the response into the list model and submits the new data to the adapter.

  1. Send a request to the API service.
  2. Receive the HTTP response.
  3. Validate the response and parse its JSON body.
  4. Map the response objects into the application's model.
  5. Submit the resulting list to the adapter.
  6. Display an appropriate state if the result is empty or unsuccessful.

When using a ListAdapter, call submitList(newItems). Avoid creating a new adapter for every response because reusing the adapter simplifies state management and improves maintainability.

14. Handling Loading, Success, Empty, and Error States

A dynamic list should clearly communicate what the application is doing. It should not leave users staring at a blank screen while data is being fetched.

State UI Behavior
Loading Show a progress indicator while the request is running.
Success Display the retrieved records.
Empty Display a message when no records are available.
Error Show a helpful message and, where appropriate, a retry action.
Offline with cached data Show saved records and optionally indicate that they may be outdated.

Example UI State

sealed interface ProductUiState {
    data object Loading : ProductUiState

    data class Success(
        val products: List<ProductEntity>
    ) : ProductUiState

    data object Empty : ProductUiState

    data class Error(
        val message: String
    ) : ProductUiState
}

The screen can observe this state from a ViewModel and decide whether to display the progress indicator, RecyclerView, empty-state message, or error message.

15. Combining API Data with a Local Database

Many applications use both a remote API and a local database. The API provides server-managed data, while the database stores a local copy that can be displayed quickly and may remain available when the network is unavailable.

A common data flow is:

  1. Observe the local database and display any saved records.
  2. Request the latest data from the API.
  3. Convert the API response into local database entities.
  4. Save the received records to Room.
  5. Allow the Room Flow to emit the updated list.
  6. Update RecyclerView automatically from the database observation.

This approach is often called an offline-first or cache-backed design, depending on how the application handles network freshness and failures.

Example Repository

class ProductRepository(
    private val api: ProductApiService,
    private val dao: ProductDao
) {
    fun observeProducts(): Flow<List<ProductEntity>> {
        return dao.observeProducts()
    }

    suspend fun refreshProducts() {
        val response = api.getProducts()

        val entities = response.products.map { product ->
            ProductEntity(
                id = product.id,
                title = product.title,
                price = product.price
            )
        }

        dao.insertProducts(entities)
    }
}

This repository demonstrates the main structure but does not include every production concern. In a real application, handle API failures, record deletions, transaction boundaries, synchronization rules, and stale-cache behavior. A failed network refresh should not automatically erase valid cached data.

16. Using a ViewModel for Dynamic Lists

A ViewModel separates UI state and data-related logic from the Activity or Fragment. This makes the code easier to test and helps preserve relevant UI state during configuration changes.

class ProductViewModel(
    private val repository: ProductRepository
) : ViewModel() {

    val products: Flow<List<ProductEntity>> =
        repository.observeProducts()

    fun refreshProducts() {
        viewModelScope.launch {
            try {
                repository.refreshProducts()
            } catch (exception: Exception) {
                // Expose a meaningful error state
                // through StateFlow in a full implementation.
            }
        }
    }
}

This example focuses on the data flow. For a production implementation, expose a structured UI state using StateFlow, distinguish connectivity errors from other failures, and avoid silently swallowing exceptions.

17. Pull-to-Refresh and Manual Refresh

A refresh action allows users to request updated data without leaving the screen. Android applications may implement this using a refresh button or SwipeRefreshLayout.

The refresh operation should:

  • Start a network request.
  • Display a refresh indicator.
  • Update the local database after a successful response.
  • Allow the database observer to update the list.
  • Stop the refresh indicator whether the request succeeds or fails.
  • Preserve cached data if the refresh fails.

Refresh actions should also avoid launching multiple unnecessary requests at the same time.

18. Implementing Pagination for Large Datasets

Loading thousands of records in a single request may consume excessive memory and increase loading time. Pagination retrieves a limited number of records at a time.

For example, a server might return 20 products per request. When the user approaches the end of the current list, the application requests the next page.

suspend fun loadProducts(
    page: Int,
    pageSize: Int
): ProductResponse {
    return apiService.getProducts(
        page = page,
        limit = pageSize
    )
}

The function above assumes that the API service defines matching query parameters. A real service could use page numbers, offsets, cursors, or continuation tokens depending on the backend.

For more advanced Android applications, the Paging library can help manage paginated data from network and database sources.

19. Searching and Filtering Database or API Results

Dynamic lists often need search and filter functionality. Local data can be filtered in Kotlin, while large datasets may benefit from database queries or server-side filtering.

Example of filtering a list of products by title:

fun searchProducts(
    products: List<ProductEntity>,
    query: String
): List<ProductEntity> {
    val text = query.trim()

    if (text.isEmpty()) return products

    return products.filter { product ->
        product.title.contains(
            text,
            ignoreCase = true
        )
    }
}

For Room, a DAO can perform search directly using SQL. For remote data, query parameters can be sent to an API if the backend supports them.

20. Common Errors and Troubleshooting

Problem Possible Cause Solution
RecyclerView is empty. The adapter is not attached or the dataset is empty. Check adapter initialization, layout manager, response contents, and list submission.
Network request fails. No connection, incorrect URL, or server error. Check connectivity, endpoint configuration, HTTP status, and server logs.
JSON parsing fails. Response structure differs from the data model. Inspect the actual response and correct the model or serialization annotations.
Application freezes. Blocking work runs on the main thread. Use coroutines and appropriate dispatchers for blocking operations.
Database list does not update. Data is not inserted or the UI does not collect the query Flow. Verify DAO operations and lifecycle-aware collection.
Duplicate records appear. Repeated inserts or incorrect primary-key handling. Use stable identifiers and appropriate conflict strategies.
Old data remains visible. The API refresh did not succeed or synchronization logic is incomplete. Expose refresh status and define a clear cache update strategy.
Data disappears offline. The UI depends entirely on the network response. Persist data locally and display the cached database results.

21. Security and Reliability Best Practices

  • Use HTTPS for network communication.
  • Never place secret API keys or private credentials directly in client-side source code.
  • Validate server responses before using the data.
  • Handle network timeouts, HTTP errors, and serialization failures.
  • Use Room instead of manually managing SQLite queries for typical structured local storage.
  • Keep database and network work off the main thread.
  • Use lifecycle-aware collection for UI data streams.
  • Apply appropriate database migrations when the schema changes.
  • Avoid displaying sensitive information in logs.
  • Define how stale records, deleted server records, and failed synchronization should be handled.

22. Testing Dynamic Lists

Testing helps ensure that data is retrieved, stored, and displayed correctly.

  • Verify that database records appear in RecyclerView.
  • Confirm that newly inserted database records update the list.
  • Test valid API responses and empty responses.
  • Test offline mode and server failures.
  • Verify loading and error indicators.
  • Check that refresh operations update cached data.
  • Test duplicate records and repeated requests.
  • Verify pagination and list ordering.
  • Test rotation or other configuration changes.
  • Ensure that long lists scroll smoothly.

23. Practical Project: Dynamic Product Catalog

Objective: Build an Android application that displays products from a remote API and stores them locally for subsequent viewing.

Project Requirements

  1. Create a Product model and Room entity.
  2. Create a DAO for reading and writing product records.
  3. Build a Retrofit API service to fetch product data.
  4. Create a repository that coordinates the API and database.
  5. Use a ViewModel to expose the current UI state.
  6. Display products in RecyclerView using ListAdapter and DiffUtil.
  7. Show loading, empty, success, and error states.
  8. Add a refresh action.
  9. Implement search by product title.
  10. Test that previously saved products remain visible when the device is offline.

Expected Outcome

The application should display products efficiently, refresh information from the server, retain cached records, and provide clear feedback when data cannot be loaded.

24. Interview Questions and Answers

Q1. What is a dynamic list in Android?

Answer: A dynamic list displays data that can change during application execution. Its contents may come from a database, API, or in-memory collection.

Q2. Why is RecyclerView used for dynamic data?

Answer: RecyclerView efficiently reuses item views and supports flexible layouts, scrolling, and adapter-based updates.

Q3. What is Room?

Answer: Room is an Android persistence library that provides an abstraction layer over SQLite and supports structured database access through entities, DAOs, and database classes.

Q4. What is a DAO?

Answer: A Data Access Object defines methods used to query, insert, update, and delete database records.

Q5. What is a REST API?

Answer: A REST API exposes resources over HTTP and commonly exchanges data using JSON. Android applications can use HTTP clients such as Retrofit to communicate with it.

Q6. Why are Kotlin coroutines useful for API calls?

Answer: Coroutines support asynchronous operations without blocking the main thread and provide structured cancellation and error-handling patterns.

Q7. What is the purpose of a repository?

Answer: A repository provides a common data-access layer that coordinates sources such as a remote API and a local database.

Q8. Why use a local database when an API is available?

Answer: A local database can provide faster access, cached data, and offline availability. It also helps the UI continue displaying saved records when the network is unavailable.

Q9. What is the benefit of ListAdapter and DiffUtil?

Answer: They calculate differences between datasets and allow RecyclerView to update changed items efficiently.

Q10. What is pagination?

Answer: Pagination retrieves a large dataset in smaller portions, reducing initial loading time and resource consumption.

Q11. How should API errors be handled?

Answer: The application should distinguish relevant failures, display a helpful message, offer retry when appropriate, and preserve valid cached data when possible.

Q12. What is the role of a ViewModel?

Answer: A ViewModel manages UI-related state and logic independently of the Activity or Fragment, helping the application survive configuration changes.

25. Quick Revision

  • Dynamic lists display changing collections of records.
  • RecyclerView is responsible for displaying scrollable item collections.
  • Adapters bind data to individual views.
  • Room stores structured data locally.
  • DAO methods define database operations.
  • Retrofit can retrieve remote API data.
  • Kotlin coroutines support asynchronous work.
  • Flow can emit updated database query results.
  • Repositories coordinate data sources.
  • ViewModels expose UI-related state.
  • ListAdapter and DiffUtil support efficient list updates.
  • Pagination helps manage large datasets.
  • Loading, empty, and error states improve user experience.

Conclusion

Displaying database and API data in dynamic lists is a fundamental Android development skill. RecyclerView provides an efficient way to present collections, Room supports local persistence, and Retrofit enables communication with remote services. Kotlin coroutines, Flow, repositories, and ViewModels help organize these operations into a maintainable architecture.

For a reliable application, combine efficient list updates with proper error handling, lifecycle-aware data collection, local caching, and testing. These techniques provide a smoother user experience and help applications remain useful even when network conditions change.


Additional Learning Resources

Android Developers: Save Data Using Room

Android Developers: Asynchronous Room Queries

Kotlin Documentation: Coroutines

Explore Android App Development Training at JustAcademy

Register for a Course Demo

Visit JustAcademy

Previous Next
whatsapp